using System;
using System.Collections.Generic;
using System.Text;
using Dart.Telnet;
using System.Text.RegularExpressions;
using System.IO;
using System.Diagnostics;
using System.Configuration;

namespace SymAutomate.HostTelnetConnection
{
    /// <summary>
    /// Handles Global Telnet functions
    /// Error Numbers 220-299; Reference file ErrorNumbers.txt for more information.
    /// 220 Error: AIX username/password invalid or command prompt '$' not found after login.
    /// 221 Error: Server did not ask for 'UserId:'.
    /// 222 Error: Server did not ask for 'Dedicate this console to this user?'.
    /// 223 Error: %initial socket connection error%
    /// 224 Error: SYM directory '%directory name%' does not exist.
    /// 225 Error: Too many invalid password attempts at this console.
    /// 226 Error: Symop Operator Id is invalid.
    /// 227 Error: 'symop' command at Aix prompt is unresponsive. This user might not have access to symop.
    /// 228 Error: System Is Not Available.
    /// 229 Error: SYM Password has expired.
    /// 230 Error: Security Override %message%
    /// 231 Error: SYM Userid/Password Invalid.
    /// 232 Error: SYM Password has expired. While attempting to renew SYM password, config file could not be found. Working directory may not be set correctly.
    /// 233 Error: SYM Password has expired. Attempt to renew failed.
    /// 234 Error: SYM Password has expired. While attempting to renew SYM password, existing userid/password is not in expected format.
    /// 235 Error: SYM Password has expired. While attempting to renew SYM password, config file was modified less than 2 minutes ago. Possibly another thread was changing the password.
    /// </summary>
    class Driver /* This does not inherit from Base driver nor implements the interface like all other drivers */
    {
        private Parameters __Parameters;

        private Telnet __Telnet;

        public Parameters Parameters { get { return __Parameters; } }

        public Driver(ConsoleArguments _ConsoleArguments)
        {
            __Parameters = new Parameters(_ConsoleArguments);
        }

        /// <summary>
        /// Defines the help text for this driver
        /// </summary>
        public static ConsoleHelp ReturnConsoleHelp()
        {
            ConsoleHelp _ConsoleHelp = new ConsoleHelp("Global Telnet Parameters");
            _ConsoleHelp.Parameters.Add("HostIp");
            _ConsoleHelp.Parameters.Add("AixUsername");
            _ConsoleHelp.Parameters.Add("AixPassword");
            _ConsoleHelp.Parameters.Add("SymopOperatorId");
            _ConsoleHelp.Parameters.Add("SymUserid");
            _ConsoleHelp.Parameters.Add("Sym");
            _ConsoleHelp.Parameters.Add("AutoRenewPassword");
            _ConsoleHelp.Parameters.Add("Command");
            _ConsoleHelp.Parameters.Add("Debug");
            return _ConsoleHelp;
        }

        public void ProcessCommand()
        {
        }

        /// <summary>
        /// Log onto Symitar AIX 
        /// </summary>
        public void AixLogin()
        {
            string s = "";

            this.TelnetCloseConnection();
            this.InitializeTelnet(__Parameters.TelnetCommandsTimeoutInSec);

            //login to the remote host server
            try
            {
                __Telnet.Connect(__Parameters.HostIp);
                __Telnet.Login(new Credentials(__Parameters.AixUsername, __Parameters.AixPassword, GlobalParameters.AIXCommandPrompt));
            }
            catch (Exception _TelnetException)
            {
                throw new ReturnException("223 Error: " + _TelnetException.Message);
            }

            this.TelnetSend("ls" + GlobalParameters.TelnetSendEndOfCommand);
            try
            {
                s = __Telnet.ReadToDelimiter("$").ToString();
            }
            catch (Dart.Telnet.DataException _DataException)
            {
                this.LogIncommingStream(_DataException.DataRead.ToString());
                throw new ReturnException("220 Error: AIX username/password invalid or command prompt '$' not found after login.");
            }
            this.LogIncommingStream(s);
        }

        /// <summary>
        /// Log onto the Symitar System until the SYMOP
        /// </summary>
        public void SymopLogin()
        {
            string s = string.Empty;

            this.AixLogin();

            this.TelnetSend("symop" + GlobalParameters.TelnetSendEndOfCommand);
            try
            {
                s = __Telnet.ReadToDelimiter("Operator Id :").ToString();
            }
            catch (Dart.Telnet.DataException _DataException)
            {
                s = this.LogIncommingStream(_DataException.DataRead.ToString());
                throw new ReturnException("227 Error: 'symop' command at Aix prompt is unresponsive. This user might not have access to symop.");
            }
            this.LogIncommingStream(s);

            this.TelnetSend(__Parameters.SymopOperatorId + GlobalParameters.TelnetSendEndOfCommand);
            try
            {
                s = __Telnet.ReadToDelimiter(GlobalParameters.SymopCommandPrompt).ToString();
            }
            catch (Dart.Telnet.DataException _DataException)
            {
                s = this.LogIncommingStream(_DataException.DataRead.ToString());
                throw new ReturnException("226 Error: Symop Operator Id is invalid.");
            }
            this.LogIncommingStream(s);
        }

        /// <summary>
        /// Log on to the Symitar System until Menu screen is rendered for the specified SYM;
        /// All exceptions are rolled up the chain
        /// </summary>
        public void Login()
        {
            string s = "";

            this.SymopLogin(); 

            this.TelnetSend("LOGON" + GlobalParameters.TelnetSendEndOfCommand);
            s = __Telnet.ReadToDelimiter("Logon to Institution").ToString();
            this.LogIncommingStream(s);

            this.TelnetSend(__Parameters.Sym + GlobalParameters.TelnetSendEndOfCommand);
            try
            {
                s = __Telnet.ReadToDelimiter("UserId :").ToString();
            }
            catch (Dart.Telnet.DataException _DataException)
            {
                s = this.LogIncommingStream(_DataException.DataRead.ToString());

                if (s.Contains("No such file or directory"))
                    throw new ReturnException("224 Error: SYM directory '" + __Parameters.Sym.ToString() + "' does not exist.");
                else if (s.Contains("Too Many Invalid Password Attempts"))
                    throw new ReturnException("225 Error: Too many invalid password attempts at this console.");
                else if (s.Contains("System Is Not Available"))
                    throw new ReturnException("228 Error: System Is Not Available.");
                else
                    throw new ReturnException("221 Error: Server did not ask for 'UserId:'.");
            }
            this.LogIncommingStream(s);

            this.TelnetSend(__Parameters.SymUserId + GlobalParameters.TelnetSendEndOfCommand);
            try
            {
                s = __Telnet.ReadToDelimiter("Dedicate this console to this user?").ToString();
            }
            catch (Dart.Telnet.DataException _DataException)
            {
                s = this.LogIncommingStream(_DataException.DataRead.ToString());

                if (s.Contains("UserId :"))
                {
                    throw new ReturnException("231 Error: SYM Userid/Password Invalid.");
                }
                else if (s.Contains("Changing Password for User"))
                {
                    if (__Parameters.AutoRenewPassword == true &&
                        System.Configuration.ConfigurationManager.AppSettings["hostip"] == __Parameters.HostIp &&
                        System.Configuration.ConfigurationManager.AppSettings["sym"] == __Parameters.Sym &&
                        System.Configuration.ConfigurationManager.AppSettings["symuserid"] == __Parameters.SymUserId)
                    {
                        try
                        {
                            string _SymPasswordOnly = string.Empty;

                            //generate the new password
                            string _NewSymUserIdPassword = this.GetNewPassword(__Parameters.SymUserId, ref _SymPasswordOnly);

                            //if encryption is enabled then encrypt the password
                            if (__Parameters.EncryptSensitiveKeys && !string.IsNullOrEmpty(__Parameters.EncryptionKey))
                            {
                                AESEncryption _AESEncryption = new AESEncryption(__Parameters.EncryptionKey);
                                _NewSymUserIdPassword = _AESEncryption.Encrypt(_NewSymUserIdPassword);
                            }

                            //write the new generated password to the config file
                            //TODO: this function call can move to GlobalFunctions later
                            this.WriteToConfigFile(__Parameters.SymUserId, _NewSymUserIdPassword, true);

                            //send keystrokes to symitar to set the new password
                            try
                            {
                                this.TelnetSend(_SymPasswordOnly + GlobalParameters.TelnetSendEndOfCommand);
                                s = __Telnet.ReadToDelimiter("Okay?").ToString();
                                this.LogIncommingStream(s);

                                this.TelnetSend("Y" + GlobalParameters.TelnetSendEndOfCommand); //Okay
                                s = __Telnet.ReadToDelimiter("Dedicate this console to this user?").ToString();
                                this.LogIncommingStream(s);
                            }
                            catch (Dart.Telnet.DataException _DataException1)
                            {
                                this.LogIncommingStream(_DataException1.DataRead.ToString());

                                //if setting of new password failed due to some reason, change the password back in config file
                                this.WriteToConfigFile(_NewSymUserIdPassword, __Parameters.SymUserId, false);
                                throw _DataException1;
                            }
                        }
                        catch (Exception _PasswordRenewException)
                        {
                            if (_PasswordRenewException is ReturnException)
                                throw _PasswordRenewException;
                            else
                                throw new ReturnException("233 Error: SYM Password has expired. Attempt to renew failed. " + _PasswordRenewException.ToString());
                        }
                    }
                    else
                    {
                        throw new ReturnException("229 Error: SYM Password has expired.");
                    }
                }
                else
                {
                    throw new ReturnException("222 Error: Server did not ask for 'Dedicate this console to this user?'.");
                }
            }

            //"Application Waiting for Approval" may show up.
            this.TelnetSend("Y" + GlobalParameters.TelnetSendEndOfCommand); //Dedicate this console to this user?
            s = __Telnet.ReadToDelimiter(new string[] { "Menu Selection", "Applications Awaiting Approval" }).ToString();
            s = this.LogIncommingStream(s);

            if (s.Contains("Applications Awaiting Approval"))
            {
                this.TelnetSend("" + GlobalParameters.TelnetSendEndOfCommand);
                s = __Telnet.ReadToDelimiter("Menu Selection").ToString();
                this.LogIncommingStream(s);
            }
        }

        /// <summary>
        /// Replaces the OLD SYM password with the NEW SYM password in the config file
        /// </summary>
        /// <param name="_OldUseridPassword"></param>
        /// <param name="_NewUseridPassword"></param>
        private void WriteToConfigFile(string _OldUseridPassword, string _NewUseridPassword, bool _CheckFileLastWriteTime)
        {
            if (File.Exists("SymAutomate.exe.config"))
            {
                if (_CheckFileLastWriteTime && 
                    File.GetLastWriteTime("SymAutomate.exe.config") >= DateTime.Now.AddMinutes(-2))
                {
                    throw new ReturnException("235 Error: SYM Password has expired. While attempting to renew SYM password, config file was modified less than 2 minutes ago. Possibly another thread was changing the password.");
                }

                string _ConfigFileContents = File.ReadAllText("SymAutomate.exe.config");
                _ConfigFileContents = _ConfigFileContents.Replace("key=\"symuserid\" value=\"" + _OldUseridPassword + "\"", "key=\"symuserid\" value=\"" + _NewUseridPassword + "\"");
                File.WriteAllText("SymAutomate.exe.config", _ConfigFileContents);
            }
            else
            {
                throw new ReturnException("232 Error: SYM Password has expired. While attempting to renew SYM password, config file could not be found. Working directory may not be set correctly.");
            }
        }

        /// <summary>
        /// This will return a new sym password by adding -00 to the end or incrementing -00 to -01, -02, etc
        /// </summary>
        /// <param name="_UseridPassword"></param>
        /// <param name="_SymPasswordOnly"></param>
        /// <returns></returns>
        private string GetNewPassword(string _UseridPassword, ref string _SymPasswordOnly)
        {
            string _Userid = null;
            string _Password = null;

            //split into userid and password
            //find the first period "."
            string[] _UseridPasswordCollection = _UseridPassword.Split('.');

            //no period found
            if (_UseridPasswordCollection.Length <= 1)
                throw new ReturnException("234 Error: SYM Password has expired. While attempting to renew SYM password, existing userid/password is not in expected format.");
            else //array size is at least 2
            {
                for (int i = 0; i < _UseridPasswordCollection.Length; i++)
                {
                    if (i == 0)
                        _Userid = _UseridPasswordCollection[i];
                    else if (i == 1)
                        _Password = _UseridPasswordCollection[i];
                    else
                        _Password += "." + _UseridPasswordCollection[i];
                }
            }

            if (_Password.Length >= 4 &&
                _Password[_Password.Length - 3] == '-' &&
                char.IsDigit(_Password[_Password.Length - 2]) &&
                char.IsDigit(_Password[_Password.Length - 1]))
            {
                int _PasswordSuffixIndex = Convert.ToInt32(_Password[_Password.Length - 2].ToString() + _Password[_Password.Length - 1].ToString());
                if (_PasswordSuffixIndex == 99)
                    _PasswordSuffixIndex = 0;
                else
                    _PasswordSuffixIndex++;
                _Password = _Password.Substring(0, _Password.Length - 3) + "-" + _PasswordSuffixIndex.ToString().PadLeft(2, '0');
            }
            else
            {
                _Password = _Password + "-00";
            }

            _SymPasswordOnly = _Password;
            return _Userid + "." + _Password;
        }

        /// <summary>
        /// Closes the Telnet Connection
        /// </summary>
        public void TelnetCloseConnection()
        {
            try
            {
                if (__Telnet.State != ConnectionState.Closed) __Telnet.Close();
            }
            catch (Exception _TelnetClose) { }
        }

        /// <summary>
        /// Sends a Telnet command to the stream
        /// </summary>
        /// <param name="s"></param>
        public void TelnetSend(string s)
        {
            //Printing this is redundant for console printing
            //Console.Write(RemoveNonPrintableCharacters(s));
            __Telnet.Write(s);
        }

        /// <summary>
        /// Waits for a string to be found in the Telnet response stream
        /// </summary>
        /// <param name="s"></param>
        /// <returns>string encapsulating the received data</returns>
        public string TelnetWaitFor(string s)
        {
            return __Telnet.ReadToDelimiter(s).ToString();
        }

        /// <summary>
        /// Waits for one of the strings to be found in the Telnet response stream
        /// </summary>
        /// <param name="s"></param>
        /// <returns>string encapsulating the received data</returns>
        public string TelnetWaitFor(string[] s)
        {
            return __Telnet.ReadToDelimiter(s).ToString();
        }

        /// <summary>
        /// Logs the incomming Telnet stream
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        public string LogIncommingStream(string s)
        {
            try
            {
                byte[] _Buffer = new byte[8192];
                string _BufferRead = __Telnet.Read(_Buffer, 0, 8192).ToString();
                s += _BufferRead ;
            }
            catch (Exception _NothingToRecieve)
            {
                //do nothing if there is nothing to recieve;
            }

            if (__Parameters.Debug == true)
                Console.Write(RemoveNonPrintableCharacters(s));
            return s;
        }

        /// <summary>
        /// Initializes the Telnet Connection
        /// </summary>
        /// <param name="_TelnetCommandsTimeoutInSec"></param>
        private void InitializeTelnet(int _TelnetCommandsTimeoutInSec)
        {
            __Telnet = new Telnet();
            this.TelnetChangeTimeout(_TelnetCommandsTimeoutInSec);
        }

        /// <summary>
        /// Changes the default Telnet Response timeout
        /// </summary>
        /// <param name="_TelnetTimoutInSec"></param>
        public void TelnetChangeTimeout(int _TelnetTimoutInSec)
        {
            __Telnet.SocketOption.ReceiveTimeout = _TelnetTimoutInSec * 1000;
            __Telnet.SocketOption.SendTimeout = _TelnetTimoutInSec * 1000;
        }

        /// <summary>
        /// Removes/Replaces the non-printable characters from the string/stream
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        public string RemoveNonPrintableCharacters(string s)
        {
            //Convert any \n\r to \r\n
            s = s.Replace("\n\r", "\r\n");
            //Convert all naked \n to \r\n (2-step process)
            s = s.Replace("\r\n", "\n");
            s = s.Replace("\n", "\r\n");
            //Replace all Tabs with spaces
            s = s.Replace("\t", "     ");
            //Remove <esc>[0m (all attributes off)
            s = s.Replace("\x1b[0m", "");
            //Remove <esc>[m (all attributes off)
            s = s.Replace("\x1b[m", "");
            s = s.Replace("\x1b[1m", "");
            s = s.Replace("\x1b[5m", "");
            //don't know what these are; could be new page or page change
            s = s.Replace("\x1b[H\x1b[J", "\r\n\r\n");

            //s = Regex.Replace(s, @"\x1b\[\d\d;\d\dH", "\r\n");

            return s;
        }

        /// <summary>
        /// Handles global exceptions
        /// </summary>
        /// <param name="e"></param>
        /// <param name="s"></param>
        public void HandleException(Exception e, string s)
        {
            s = this.RemoveNonPrintableCharacters(s);

            //match the regex to see if the last telnet response (s) contains "Security Override"
            MatchCollection _MatchCollection;
            Regex _Regex = new Regex(@"(Security Override)(.+)", RegexOptions.IgnoreCase);
            _MatchCollection = _Regex.Matches(s);
            if (_MatchCollection.Count > 0)
            {
                throw new ReturnException("230 Error: " + _MatchCollection[0].Groups[1].Value + _MatchCollection[0].Groups[2].Value.Replace(" :", ""));
            }

            //other global exceptions can be checked for display custom messages

            //this will preserve the stack trace before we re-throw the exception
            System.Reflection.MethodInfo _MethodInfo = typeof(Exception).GetMethod("InternalPreserveStackTrace", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
            _MethodInfo.Invoke(e, null);

            throw e;
        }

        /// <summary>
        /// Pass through method for raw Telnet receive
        /// </summary>
        /// <returns></returns>
        public Segment Receive()
        {
            byte[] _Buffer = new byte[8192];
            __Telnet.Read(_Buffer, 0, 8192);
            return new Segment(_Buffer, 0, 8192);
        }
    }
}
